﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
using ObjectTransposeApi.Handler.Expand;
using ObjectTransposeApi.Structure;
using System.Collections;
using ObjectTransposeApi.Handler.Property;
using ObjectTransposeApi.Extension;
using ObjectTransposeApi.Helper;

namespace ObjectTransposeApi
{
    /// <summary/>
    public static class ObjectToIndexedList
    {
        /// <summary/>
        public static List<T> Copy<T>(
            object input,
            string indexName,
            params AbstractMapper[] mappers) where T : new()
        {
            PropertyMapper[] propertyMappers = Array.FindAll(mappers, value => typeof(PropertyMapper).IsInstanceOfType(value)).Select(value => (PropertyMapper)value).ToArray();
            IndexMapper[] indexMappers = Array.FindAll(mappers, value => typeof(IndexMapper).IsInstanceOfType(value)).Select(value => (IndexMapper)value).ToArray();
            TypeMapper[] typeMappers = Array.FindAll(mappers, value => typeof(TypeMapper).IsInstanceOfType(value)).Select(value => (TypeMapper)value).ToArray();

            AbstractExpandHandler expandHandler = ChainHelper.FetchExpandHandlers();
            
            List<T> destinations = new List<T>();

            PropertyInfo destinationIndexPropertyInfo = ReflectionHelper.FetchGetProperty(
                typeof(T),
                indexName);

            // if not found then exit
            if (destinationIndexPropertyInfo == null)
            {
                return destinations;
            }

            Queue<QueueStructure> queueStructures = new Queue<QueueStructure>();

            // the fist thing that needs to be is expand the input variable ... if it is a collection then
            // each item in the collection needs to tested for a match against the destination object
            foreach (object source in expandHandler.Process(input))
            {
                PropertyInfo sourceIndexPropertyInfo = ReflectionHelper.FetchGetProperty(
                    source.GetType(),
                    indexName);

                // if a matching index property is not found then move on
                if (sourceIndexPropertyInfo == null)
                {
                    continue;
                }

                // compare types
                if (sourceIndexPropertyInfo.PropertyType != destinationIndexPropertyInfo.PropertyType)
                {
                    continue;
                }

                // fetch the source
                object sourceIndexValue = ReflectionHelper.FetchPropertyValue(
                    sourceIndexPropertyInfo, 
                    source);
         
                // nothing to do if null
                if (sourceIndexValue == null)
                {
                    continue;
                }

                T matchingDestination = default(T);

                foreach (T destination in destinations)
                {
                    // check values ... use equals so the comparison is done on the actual type and not the boxed type
                    if (!sourceIndexValue.Equals(ReflectionHelper.FetchPropertyValue(destinationIndexPropertyInfo, destination)))
                    {
                        continue;
                    }

                    // record the value
                    matchingDestination = destination;
                }

                // if no match then create
                if (matchingDestination == null)
                {
                    matchingDestination = new T();

                    // add it
                    destinations.Add(matchingDestination);
                }

                // add the items .. the getter property on the destination is needed to handle collections
                queueStructures.Enqueue(new QueueStructure(
                    matchingDestination,
                    ReflectionHelper.FetchGetSetProperties(matchingDestination.GetType()),
                    source,
                    ReflectionHelper.FetchGetSetProperties(source.GetType())));
            }

            AbstractPropertyHandler propertyHandler = ChainHelper.FetchPropertyHandler();

            // process each queued item
            while (queueStructures.Count > 0)
            {
                QueueStructure queueStructure = queueStructures.Dequeue();

                // enum the destination properties
                foreach (PropertyInfo destinationPropertyInfo in queueStructure.DestinationPropertyInfos)
                {
                    // find a matching source property
                    PropertyInfo sourcePropertyInfo = ReflectionHelper.FetchMatchingProperty(
                        queueStructure.SourcePropertyInfos,
                        destinationPropertyInfo.Name,
                        propertyMappers);

                    // if a matching source property is not found then move on
                    if (sourcePropertyInfo == null)
                    {
                        continue;
                    }

                    IndexMapper indexMapper = Array.Find(indexMappers, value => value.SourceName == sourcePropertyInfo.Name);

                    // build the structure needed by the handler
                    PropertyStructure propertyStructure = new PropertyStructure()
                    {
                        ExpandCallback = expandHandler.Process,
                        Destination = queueStructure.Destination,
                        DestinationPropertyInfo = destinationPropertyInfo,
                        Source = queueStructure.Source,
                        SourcePropertyInfo = sourcePropertyInfo,
                        IndexMapper = indexMapper,
                        TypeMappers = typeMappers,
                    };

                    // process the structure and add new items if any
                    queueStructures.EnqueueRange(propertyHandler.Process(propertyStructure));
                }
            }

            // return the object
            return destinations;
        }
    }
}
